/*            Copyright(c) 2006-2008 Dave Sexton           *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using System.ComponentModel;

namespace DaveSexton.DocProject.Sandcastle.TopicManagement
{
	[Serializable]
	public abstract class TopicFilter : ITopicFilter, INotifyPropertyChanged
	{
		#region Public Properties
		public abstract string DisplayValue { get; }

		public abstract Type EditorType { get; }

		public virtual string Name
		{
			get
			{
				return ComponentDescriptor.GetDisplayName(this.GetType());
			}
		}

		public string Memo
		{
			get
			{
				return settings.Read<string>("memo", null);
			}
			set
			{
				if (!string.Equals(value, Memo, StringComparison.Ordinal))
				{
					if (value == null)
						settings.Remove("memo");
					else
						settings.Write("memo", value);

					NotifyPropertyChanged("Memo");
				}
			}
		}

		public bool Include
		{
			get
			{
				return settings.Read("include", false);
			}
			set
			{
				if (value != Include)
				{
					settings.Write("include", value);

					NotifyPropertyChanged("Include");
				}
			}
		}

		public bool AutoApply
		{
			get
			{
				return settings.Read("build", true);
			}
			set
			{
				if (value != AutoApply)
				{
					settings.Write("build", value);

					NotifyPropertyChanged("AutoApply");
				}
			}
		}
		#endregion

		#region Private / Protected
		protected SettingsDictionary<string, string> Settings
		{
			get
			{
				return settings;
			}
		}

		private readonly bool initializing = true;
		private readonly SettingsDictionary<string, string> settings = new SettingsDictionary<string, string>(
			System.Globalization.CultureInfo.CurrentCulture, StringComparer.CurrentCulture);
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="TopicFilter" /> class for derived types.
		/// </summary>
		protected TopicFilter()
		{
			// see the clone constructor overload for more information about the purpose of this field
			initializing = false;
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="TopicFilter" /> class for derived types.
		/// </summary>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]		// initialize field prevents this
		protected TopicFilter(TopicFilter clone)
		{
			if (clone == null)
				throw new ArgumentNullException("clone");

			this.Memo = clone.Memo;
			this.Include = clone.Include;
			this.AutoApply = clone.AutoApply;

			// this field supresses property changed events while it's true
			// it's required since OnPropertyChanged is a virutal method and therefore shouldn't be called from a constructor.
			initializing = false;
		}
		#endregion

		#region Methods
		/// <exception cref="XmlException">Thrown when the specified XML <paramref name="file"/> is invalid.</exception>
		/// <exception cref="System.IO.DirectoryNotFoundException">Thrown when the specified <paramref name="file"/> cannot be found.</exception>
		/// <exception cref="System.IO.FileNotFoundException">Thrown when the specified <paramref name="file"/> cannot be found.</exception>
		[System.Diagnostics.DebuggerHidden]
		public static TopicFilterCollection LoadFilters(string file)
		{
			TopicFilterCollection filters = new TopicFilterCollection();
			filters.BeginInit();

			XmlReaderSettings settings = new XmlReaderSettings();
			settings.IgnoreComments = true;
			settings.IgnoreProcessingInstructions = true;
			settings.IgnoreWhitespace = true;

			using (XmlReader reader = XmlReader.Create(file, settings))
			{
				reader.ReadStartElement("filters", "");

				do
				{
					if (reader.IsStartElement("filter", ""))
					{
						string type = reader.GetAttribute("type", "");
						string assembly = null;

						if (!string.IsNullOrEmpty(type))
						{
							Type filterType = Type.GetType(type);

							if (filterType == null)
							{
								assembly = reader.GetAttribute("assembly", "");

								if (!string.IsNullOrEmpty(assembly))
									filterType = System.Reflection.Assembly.LoadFrom(assembly).GetType(type);
							}

							if (filterType == null)
								throw new InvalidOperationException(string.Format(System.Globalization.CultureInfo.CurrentCulture, 
									Resources.Errors.InvalidDynamicFilterType, Environment.NewLine, type, assembly, file));

							if (typeof(ITopicFilter).IsAssignableFrom(filterType))
							{
								ITopicFilter filter = (ITopicFilter) Activator.CreateInstance(filterType);

								using (XmlReader filterReader = reader.ReadSubtree())
								{
									filter.Load(filterReader);
								}

								filters.Add(filter);
							}
						}
					}
					else if (reader.IsStartElement())
					{
						IXmlLineInfo info = (IXmlLineInfo) reader;

						if (info.HasLineInfo())
							throw new XmlException(Resources.Errors.InvalidTopicFiltersFileElementName,
								null, info.LineNumber, info.LinePosition);
						else
							throw new XmlException(Resources.Errors.InvalidTopicFiltersFileElementName);
					}
				}
				while (reader.Read());
			}

			filters.EndInit();
			return filters;
		}

		public static void SaveFilters(TopicFilterCollection filters, string file)
		{
			if (filters == null)
				throw new ArgumentNullException("filters");

			XmlWriterSettings settings = new XmlWriterSettings();
			settings.Indent = true;

			using (XmlWriter writer = XmlWriter.Create(file, settings))
			{
				writer.WriteStartDocument();
				writer.WriteStartElement("filters");

				foreach (ITopicFilter filter in filters)
				{
					writer.WriteStartElement("filter");

					Type type = filter.GetType();

					if (System.Reflection.Assembly.GetExecutingAssembly() == type.Assembly)
						writer.WriteAttributeString("type", type.FullName);
					else
					{
						System.Reflection.Assembly assembly = type.Assembly;

						if (assembly.GlobalAssemblyCache)
							writer.WriteAttributeString("type", type.AssemblyQualifiedName);
						else
						{
							writer.WriteAttributeString("type", type.FullName);
							writer.WriteAttributeString("assembly", assembly.CodeBase);
						}
					}

					filter.Save(writer);

					writer.WriteEndElement();
				}

				writer.WriteEndElement();
				writer.WriteEndDocument();
			}
		}

		public abstract bool IsMatch(Topic topic);

		public abstract bool IsMatch(ReflectionApiElement element);

		public abstract ITopicFilter Clone();

		public virtual void Load(XmlReader reader)
		{
			if (reader == null)
				throw new ArgumentNullException("reader");

			reader.ReadStartElement("filter");

			while (reader.IsStartElement())
			{
				string name = reader.LocalName;

				LoadSetting(reader, name);

				if (reader.NodeType == XmlNodeType.EndElement)
					reader.Read();
			}
		}

		protected virtual void LoadSetting(XmlReader reader, string name)
		{
			settings[name] = FixupNewLinesInXmlText(reader.ReadElementString());
		}

		protected static string FixupNewLinesInXmlText(string text)
		{
			if (string.IsNullOrEmpty(text))
				return text;
			else
				// XmlReader will correctly preserve entitized new lines; however, because 
				// a user may author a document in an editor that doesn't write entitized 
				// line breaks, this code first ensures that all entitized breaks are 
				// collapsed to what a normalizing XmlReader will parse (i.e., a single \n)
				// and then each normalized value is converted to a valid new line for the
				// current environment (i.e., \r\n on Windows).  For this reason the 
				// NewLineHandling property will not be able to cover all scenarios so this
				// code is used instead to ensure that XML documents can be edited manually.
				return text.Replace("\r\n", "\n").Replace("\n", Environment.NewLine);
		}

		public virtual void Save(XmlWriter writer)
		{
			if (writer == null)
				throw new ArgumentNullException("writer");

			WriteAttributes(writer);

			foreach (KeyValuePair<string, string> pair in settings)
			{
				string key = pair.Key;

				if (!string.IsNullOrEmpty(key))
				{
					writer.WriteStartElement(key, "");

					WriteSetting(writer, key, pair.Value);

					writer.WriteEndElement();
				}
			}
		}

		protected virtual void WriteAttributes(XmlWriter writer)
		{
			// for derived classes
		}

		protected virtual void WriteSetting(XmlWriter writer, string name, string value)
		{
			writer.WriteValue(value ?? string.Empty);
		}

		protected void NotifyPropertyChanged(string propertyName)
		{
			if (!initializing)
				OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
		}
		#endregion

		#region INotifyPropertyChanged Members
		private readonly object PropertyChangedEventLock = new object();
		private PropertyChangedEventHandler PropertyChangedEvent;

		/// <summary>
		/// Event raised after a property value has changed.
		/// </summary>
		public event PropertyChangedEventHandler PropertyChanged
		{
			add
			{
				lock (PropertyChangedEventLock)
				{
					PropertyChangedEvent += value;
				}
			}
			remove
			{
				lock (PropertyChangedEventLock)
				{
					PropertyChangedEvent -= value;
				}
			}
		}

		/// <summary>
		/// Raises the <see cref="PropertyChanged" /> event.
		/// </summary>
		/// <param name="e"><see cref="EventArgs" /> object that provides the arguments for the event.</param>
		protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
		{
			PropertyChangedEventHandler handler = null;

			lock (PropertyChangedEventLock)
			{
				handler = PropertyChangedEvent;
			}

			if (handler != null)
				handler(this, e);
		}
		#endregion
	}
}
